Skip to content

[pull] main from TryGhost:main#1125

Merged
pull[bot] merged 26 commits intocode:mainfrom
TryGhost:main
May 7, 2026
Merged

[pull] main from TryGhost:main#1125
pull[bot] merged 26 commits intocode:mainfrom
TryGhost:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 7, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

allouis and others added 26 commits May 7, 2026 12:41
ref https://linear.app/ghost/issue/HKG-1768/

The existing tests for posts-stats-service._enrichWithTitles only
exercise the truthy branch — getResource() returning a valid resource.
The falsy branch (path doesn't resolve, url_exists must be false) is
the side most likely to regress when the call switches from the sync
getResource() to the async facade.resolveUrl(). Pinning it here means
the migration commit cannot land without preserving that contract.
Skip Ember test and hinting trees during normal development builds to speed up `pnpm dev` usage. This reduces dev server peak memory usage with ~1.1GB (~41%) and startup time with ~12s (~37%).

The Ember `/tests` support available with `EMBER_INCLUDE_TESTS=true`.
no issue

## Summary

- changed ShareModal from a monolithic props API into slot components
for Root, Trigger, Content, headers, preview, actions, social links, and
copy controls
- updated PostShareModal and the onboarding share publication dialog to
compose the new slots directly
- moved data attributes onto ShareModal.Content so callers use normal
JSX attributes instead of narrow prop bags
- updated ShareModal stories and exported the preview props type

## Why

The previous contentProps escape hatch leaked internal DialogContent
details and required narrow type patches for data attributes. The slot
API gives future share modal use-cases direct access to the DOM surfaces
they need without growing a long list of pass-through props.
ref https://linear.app/ghost/issue/BER-3471/fix-default-members-list-query-performance-with-a-composite-index

Added a composite index for the default members list query. With 2m members locally cold queries went from ~7s to ~0.1s and hot queries from ~0.8s to ~0.03s.
no-issue

Node's MODULE_NOT_FOUND errors include a "Require stack" that lists the
calling file's path. The existing heuristic checked whether the adapter
path appeared anywhere in err.message to distinguish "adapter not found"
from "adapter found but has a missing dependency." Because the adapter's
own path always appears in the require stack, the check always matched,
causing missing-dep errors to be silently swallowed and replaced with a
misleading "Unable to find adapter" message. Restricting the check to
the first line of the error message fixes the false positive.
no-issue

These were removed in 8f2ec8d as knip flagged them unused, but
SchedulingPro (the Ghost(Pro) scheduling adapter, copied in at release
time) depends on both packages. Without them, Ghost(Pro) crashes at
boot with "Unable to find scheduling adapter SchedulingPro."
ref https://linear.app/ghost/issue/BER-3615
ref #27624
ref #27703

- the "Paid subscriptions" bar chart and "Paid subscription breakdown"
pie chart were counting gift redemptions as signups and gift end-of-life
events as cancellations, which mixed gift activity into charts that are
meant to track paid Stripe activity only
- gifts now flow through their own member status (`gift`) and surface in
the Paid members KPI tooltip instead, so duplicating them in the per-
cadence/per-tier breakdowns double-counted activity and made the bar and
pie charts disagree
- reverted the gift-aware additions to subscription-stats-service so
`/stats/subscriptions` returns paid Stripe deltas and counts only;
`gift`→`paid` upgrades still appear via the regular paid subscription
event flow once the trial converts and MRR begins
ref https://linear.app/ghost/issue/BER-3614/
ref #27759

- the "Paid subscription breakdown" pie chart was rendering a
Complimentary slice derived from the comped delta, which mixed
member-status semantics into a chart meant to track paid Stripe
subscription activity only
- this also caused the pie chart's total to disagree with the "Paid
subscriptions" bar chart (which has always been paid-only); with this
change the two charts tie out as the issue requires
- comp activity is still surfaced in the Paid members KPI tooltip, so no
signal is lost for sites that grant complimentary access
- removed the now-pointless `calculateStatusSignups` helper that was
generalised to support `gift`+`comped` during the gift work but only
ever took `'comped'` after gifts moved to the backend
ref https://linear.app/ghost/issue/BER-3580/remove-ember-members-implementation

The React members list now owns list, filter, import, and export flows, so the old Ember surface and unused dependencies can be removed.
…#27764)

ref https://linear.app/ghost/issue/BER-3591/adding-a-second-label-filter-does-not-work

The add-filter inline picker rendered multiselect fields (label, tier_id,
offer_redemptions) as a sticky multi-select that stayed open after each
click. Selecting a single value worked, but the open picker invited extra
clicks, and each additional click left a stranded filter behind with stale
single-value selections while a fresh filter was created with the latest
cumulative selection. In multi-filter mode (allowMultiple={true}) the
picker now treats multiselect fields as single-select: one click commits a
new single-value filter and closes the picker. Multi-value editing of an
existing filter continues through the filter row's own picker, which
already supports live multi-select. Single-filter mode is unchanged.
towards https://linear.app/ghost/issue/NY-1260

This adds a temporary fake database with the automation tables we expect
to add. Rather than doing proper migrations, we want to have this
testbed we can use. (Of course, we'll do the proper migrations soon,
once we've validated this design!)

This should hopefully unblock building backend endpoints and therefore
frontend UIs.
no ref

We don't need to import `URL` from `url` any more.
…and mobile (#27770)

ref https://linear.app/ghost/issue/BER-3600/design-iteration

- Simplified the selection screen when only one paid tier is available
- Surfaced tier descriptions in the selection and details views
- Pinned the gift card preview while details scroll
- Removed "Powered by Ghost" branding from gift screens
- Tightened spacing on the selection screen
- Improved mobile styling across the 50/50 layouts
- Replaced the expiry date on the gift card with gift value
closes https://linear.app/ghost/issue/BER-3616

When a recipient redeems a gift subscription, Ghost sends staff a
notification email. Today it reuses the *paid subscription started* copy
and the *New paid members* email-preference toggle.

We've decided to differentiate the gift redemption staff notification
more clearly from the new paid members one. This PR changes the copy of
the gift redemption staff notification and moves it under the "Gift
subscriptions" email-preference toggle.

## Changes

**Email copy** — `notifyGiftSubscriptionStarted`:
- Subject: `🎁 Paid subscription started: <name>` → `🎁 Gift subscription
redeemed: <name>`
- Headline: `You have a new paid subscriber` → `A gift subscription was
redeemed`
- Plaintext body line updated to match
- Preview text updated to match

**Notification preference** — gift redemptions now go through the
existing **Gift subscriptions** toggle (Stripe + `giftSubscriptions`
feature flag), instead of *New paid members*:
- Toggle label: `Gift subscription purchases` → `Gift subscriptions`
- Description: `Every time someone purchases a gift subscription` →
`Every time someone purchases or redeems a gift subscription`

**Pre-commit hygiene:**
- Added a targeted `secretlint-disable-next-line` on the
random-password-generator default in `user.js` — the new secret-scanning
hook (added in #27609) flags `password: security.identifier.uid(50)` as
a credential-assignment false-positive, blocking any future change to
this file. The line generates a random password placeholder; it isn't a
real credential.
ref https://linear.app/ghost/issue/HKG-1761/

The lazy URL service evaluates permalink templates against the resource
itself (slug, published_at, primary_tag, ...). The old contract,
`getUrlByResourceId(post.id)`, only carries an id; passing the full post
object is the prerequisite for letting the lazy backend reach those fields
without an extra DB lookup. Eager-mode behaviour is unchanged.

Adds a tiny `toPlain(modelOrObj)` helper so callers can hand in either a
Bookshelf model instance or an already-serialised hash and the URL helpers
get a plain object regardless. Without this, `{...model}` would silently
drop prototype-defined fields like `id` on Bookshelf models.
ref https://linear.app/ghost/issue/HKG-1763/

The facade gives every caller a stable, resource-based interface
(`getUrlForResource(resource, options)`, `ownsResource(routerId, resource)`,
`resolveUrl(path)`, `getResourceById(id)`) so we can later swap the eager
precomputing UrlService for an on-demand backend without touching every
caller. This commit only introduces the facade; subsequent commits move
callers across, and HKG-1771 wires in the lazy backend behind a config
flag. Eager behaviour is unchanged.
ref https://linear.app/ghost/issue/HKG-1764/

RouterManager now holds a reference to the facade rather than the raw
eager UrlService, and the routing controllers (entry, channel,
collection, static, taxonomy, previews, email-post) plus rss/generate-feed
go through `routerManager.getUrlForResource(resource, ...)` /
`ownsResource(routerId, resource)`.

This is the first batch of caller migrations from the id-based
`getUrlByResourceId` API to the resource-based facade — required so the
lazy backend can later read permalink-template fields off the resource
without hitting the DB twice.
ref https://linear.app/ghost/issue/HKG-1765/

`output/utils/url.js`'s `forPost`/`forUser`/`forTag` helpers now call
`urlService.facade.getUrlForResource({...attrs, id, type}, ...)` instead
of `urlService.getUrlByResourceId(id, ...)`. The explicit `id` is critical:
Content API requests like `?fields=url` strip every attribute except url,
so a plain `{...attrs, type}` would send an id-less resource and the
eager facade's id-based fallback would return `/404/` for every record.

Unit tests pin both the standard call and the stripped-attrs case so the
regression class is caught at the serializer boundary.
ref https://linear.app/ghost/issue/HKG-1766/

`{{tags}}` / `{{authors}}` helpers and the `meta/url` and `meta/author-url`
generators now go through `urlService.facade.getUrlForResource(resource, ...)`
rather than `urlService.getUrlByResourceId(id, ...)`. Theme rendering hands
us the full resource object, so spreading it into the facade is a direct
fit and matches the contract the lazy backend will rely on.

Drops three router-manager pass-throughs (`owns`, `getUrlByResourceId`,
the no-longer-used routerManager-level `getResourceById` was kept on the
facade for the entry controller's resource-type check) along with their
last callers.
ref https://linear.app/ghost/issue/HKG-1767/

Last batch of `getUrlByResourceId(id)` callers in the backend: the slack
notifier, the IndexNow notifier, the comments-emails service, and the
audience-feedback service now call `urlService.facade.getUrlForResource`
with a full resource. Each call site already has the model in hand so
the spread is direct.

After this commit no in-tree caller invokes `urlService.getUrlByResourceId`
on the eager service; the legacy method only survives behind the facade
as the eager fall-through implementation of `getUrlForResource`.
ref https://linear.app/ghost/issue/HKG-1768/

`url-translator.getTypeAndIdFromPath`, `posts-stats-service`,
`content-stats-service`, and `mentions/resource-service` now consume the
flat resource shape returned by `urlService.facade.resolveUrl(path)`
instead of the legacy `{config: {type}, data: {...}}` envelope from
`urlService.getResource(path)`. The translator and content-stats helper
become async to match resolveUrl's contract; their direct callers are
already in async contexts so the ripple stops there.

The facade also resolves an inconsistency: the routing-level type
('posts'/'pages'/'tags'/'authors') wins over any DB type field on the
underlying resource data, so the flat resource is unambiguous.
no ref

Preview requests can include request-specific theme data, so these
responses should not be stored or reused by frontend caches.
Ref https://linear.app/ghost/issue/BER-3598/

Adding a Shade calendar component and replacing the existing native
datepicker for filtering with it.

---------

Co-authored-by: Kevin Ansfield <kevin@ghost.org>
@pull pull Bot locked and limited conversation to collaborators May 7, 2026
@pull pull Bot added the ⤵️ pull label May 7, 2026
@pull pull Bot merged commit 76982f0 into code:main May 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants